var MSPF = 1000 / 60,
    // ms/frame (FPS: 60)
    KEEP_LOOP = 500,


    /**
     * @typedef {Object} task
     * @property {Event} event
     * @property {function} listener
     */

    /** @type {task[]} */
    tasks = [];

var requestAnim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
        return setTimeout(callback, MSPF);
    },
    cancelAnim = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || window.msCancelAnimationFrame || function (requestID) {
        return clearTimeout(requestID);
    };

var lastFrameTime = Date.now(),
    requestID = void 0;

function step() {
    var called = void 0,
        next = void 0;

    if (requestID) {
        cancelAnim.call(window, requestID);
        requestID = null;
    }

    tasks.forEach(function (task) {
        var event = void 0;
        if (event = task.event) {
            task.event = null; // Clear it before `task.listener()` because that might fire another event.
            task.listener(event);
            called = true;
        }
    });

    if (called) {
        lastFrameTime = Date.now();
        next = true;
    } else if (Date.now() - lastFrameTime < KEEP_LOOP) {
        // Go on for a while
        next = true;
    }
    if (next) {
        requestID = requestAnim.call(window, step);
    }
}

function indexOfTasks(listener) {
    var index = -1;
    tasks.some(function (task, i) {
        if (task.listener === listener) {
            index = i;
            return true;
        }
        return false;
    });
    return index;
}

var AnimEvent = {
    /**
     * @param {function} listener - An event listener.
     * @returns {(function|null)} A wrapped event listener.
     */
    add: function add(listener) {
        var task = void 0;
        if (indexOfTasks(listener) === -1) {
            tasks.push(task = { listener: listener });
            return function (event) {
                task.event = event;
                if (!requestID) {
                    step();
                }
            };
        }
        return null;
    },
    remove: function remove(listener) {
        var iRemove = void 0;
        if ((iRemove = indexOfTasks(listener)) > -1) {
            tasks.splice(iRemove, 1);
            if (!tasks.length && requestID) {
                cancelAnim.call(window, requestID);
                requestID = null;
            }
        }
    }
};